iT邦幫忙

2025 iThome 鐵人賽

DAY 22
1
Software Development

消除你程式碼的臭味系列 第 22

Day 22- 輸入檢查:在處理前先驗證

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250903/20124462P1N8QjGguI.png

消除你程式碼的臭味 Day 22- 輸入檢查:在處理前先驗證

在做任何事前,先確保輸入是可信的。

想像你的每一個函式、每一個 API 端點,都是一座戒備森嚴的堡壘。而傳入的參數,就是試圖通過城門的陌生人。

你的驗證程式碼,就是城門口的衛兵。衛兵的職責不是友善地假設每個陌生人都是好人。
衛兵的職責是保持偏執、充滿懷疑,把每個陌生人都當成潛在的間諜或破壞者來盤問,檢查他們所有的證件。

一個疏忽可能讓惡意攻擊者長驅直入,SQL 注入 (SQL Injection)跨網站指令碼 (XSS) 等成為重大的安全漏洞。
只有通過了這道嚴格的盤查,這些資料才被允許進入你那乾淨、受信任的堡壘內部。

壞資料進來,壞結果出去。

經典案例:不檢查就處理

這段程式碼,就是一座沒有衛兵、城門大開的堡壘。
它天真地相信呼叫者永遠會傳入兩個合法的數字,而且分母永遠不為零。
在實際情況裡會被無情地利用,直到系統在生產環境中崩潰。

// 🔴 臭味而且是危險的那種
// 它盲目地信任輸入,這是在玩火。
function divide(a, b) { 
  return a / b; 
}

// divide(10, 0) -> Infinity
// divide(10, "hello") -> NaN
// divide(null, 2) -> 0 (這是你想要的嗎?)
  • 它推遲了爆炸: 當你傳入 (10, 0)divide 函式本身沒有立刻爆炸。它默默地回傳了一個 Infinity 這個鬼東西。這個 Infinity 可能會被傳遞給後續十個函式,直到最後某個地方因為無法處理它而崩潰。屆時,你得回溯一長串的呼叫鏈,才能找到最初的罪魁禍首。

  • 它的錯誤沒有任何幫助: InfinityNaN 這種回傳值,沒有告訴呼叫者他到底做錯了什麼。

  • 它沒有定義契約: 這個函式沒有跟它的使用者立下清晰的契約。它沒有明確說明「我需要什麼」以及「當你不遵守規則時會發生什麼」。

https://ithelp.ithome.com.tw/upload/images/20250924/20124462C0t0bsukJk.png

先驗證再處理

好的程式碼充滿了「健康的偏執」。
它在執行任何核心邏輯之前,會先在入口處設定層層關卡衛兵模式 (保護式敘述) Guard Clauses

// 🟢 好衛兵:這是一座有嚴謹看守的堡壘。

function divide(a, b) {
  // 衛兵 1: 檢查型別證件
  if (typeof a !== 'number' || typeof b !== 'number') {
    throw new TypeError('Both arguments must be numbers.');
  }
  
  // 衛兵 2: 檢查業務規則
  if (b === 0) {
    throw new RangeError('Divisor cannot be zero.');
  }

  // --- 通過所有檢查後,才來到受信任的堡壘核心 ---
  return a / b;
}
  • 快速失敗,大聲呼救 (Fail Fast, Fail Loud): 一旦發現輸入有問題,程式就立刻、馬上在入口處拋出錯誤,並且用一個極其清晰的錯誤訊息大聲地告訴呼叫者「你做錯了什麼」。這讓除錯變得異常簡單。

  • 主要邏輯的純淨: return a / b; 這行主要邏輯,現在可以 100% 信任 ab 是安全的。它不需要再做任何防禦性檢查了。

  • 清晰的契約: 專業的 API 設計函式契約非常清楚:「你必須給我兩個數字,且第二個不能為零。如果你違反契約,我會立刻把你踢出去,並告訴你原因。」

https://ithelp.ithome.com.tw/upload/images/20250924/20124462AZmNaJ4j9i.png

建立你的防禦體系:輸入驗證的主要原則

驗證的邊界:伺服器端 vs. 客戶端

一個常見的誤區是只在前端做驗證。

  • 客戶端驗證 (前端):主要目的是提升使用者體驗 (UX),提供即時的回饋,避免使用者提交無效的表單。

  • 伺服器端驗證 (後端):主要目的是保障系統安全與資料完整性

客戶端的驗證可以被輕易繞過,因此伺服器端的驗證是絕對不可或缺的最後防線

驗證 (Validation) vs. 消毒 (Sanitization)

這是兩個相輔相成但不同的概念:

  • 驗證 (Validation):是拒絕不符合規則的資料。

    • ex: 年齡欄位傳入 -10,驗證的職責是直接拋出錯誤。
  • 消毒 (Sanitization):是清理可接受但不夠乾淨的資料。

    • ex:使用者名稱欄位傳入 " John Doe "(後面有空格),消毒的職責是將其處理成 "John Doe"

處理順序:先驗證,再消毒,最後才使用資料。

Schema 驅動驗證

與其手寫大量的 if/else,不如使用 Zod/Joi 這類工具,將驗證規則集中定義為一個「Schema」(綱要)。
可以成立系統的「單一事實來源」,讓驗證更簡單,還能自動生成 TypeScript 型別,並用於自動化測試。

測試要點

  • 覆蓋案例:必須包含正向 (通過)、反向 (失敗)、邊界值 (null、空字串、0min/max) 的測試。

  • 驗證訊息:斷言 (Assert) 回傳的錯誤訊息是否精準、符合預期。

檢查清單

  • 基礎檢查:是否已驗證輸入的型別、格式與範圍?

  • 存在性檢查:是否已確保所有必填欄位都存在,且不為 null 或空字串?

  • 安全性考量:目前的檢查是否足以防禦已知的惡意輸入模式(如腳本注入)?

  • 錯誤回饋:回傳的錯誤訊息是否清晰、具體,能引導使用者修正?

  • 預設拒絕:系統是否在處理任何邏輯前,就立即拒絕來源不可信的輸入?

今日重點

  • 將驗證視為安全的第一道防線,並在處理任何邏輯前執行。

  • 讓錯誤快速、大聲、且清晰可預測,這能簡化除錯。

  • 杜絕髒資料,讓你的核心邏輯只處理乾淨、可信的資料。

  • 永遠不要信任客戶端,伺服器端驗證是絕對必要的。


上一篇
Day 21- 註解:讓程式碼自己說話
下一篇
Day 23- 空值處理:別回傳 null,用更安全的回應
系列文
消除你程式碼的臭味23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言